########################################################################
#  Searx-Qt - Lightweight desktop application for Searx.
#  Copyright (C) 2020-2022  CYBERDEViL
#
#  This file is part of Searx-Qt.
#
#  Searx-Qt is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Searx-Qt is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################


# Notes:
# https://docs.python.org/3/library/json.html
# https://docs.python.org/3/library/os.path.html
# https://docs.python.org/3/library/sysconfig.html


from PyQt5.QtWidgets import QApplication, QStyleFactory
from PyQt5.QtCore import QResource

import os
import sysconfig  # Get path prefix (example: /usr).
import json  # Theme manifest file is json.


from searxqt import THEMES_PATH
from searxqt.core.log import error, debug, warning


# Paths
USER_THEMES_PATH = os.path.join(
    sysconfig.get_config_var('userbase'),
    'share/',
    THEMES_PATH
)
SYS_THEMES_PATH = os.path.join(
    sysconfig.get_config_var('prefix'),
    'share/',
    THEMES_PATH
)


def replaceFileExt(filePath, ext):
    return os.path.splitext(filePath)[0] + ext


class UserTheme:
    def __init__(
        self,
        key,
        name,
        cssFile,
        path,
        icons=None,
        resultsCssFile=None,
        failCssFile=None
    ):
        self.__key = key
        self.__name = name
        self.__path = path
        self.__icons = icons
        self.__cssFile = cssFile
        self.__resultsCssFile = resultsCssFile
        self.__failCssFile = failCssFile

    @property
    def key(self):
        """ This is also the directory name of the theme.
        """
        return self.__key

    @property
    def name(self):
        return self.__name

    @property
    def cssFile(self):
        return self.__cssFile

    @property
    def path(self):
        return self.__path

    @property
    def icons(self):
        return self.__icons

    @property
    def resultsCssFile(self):
        return self.__resultsCssFile

    @property
    def failCssFile(self):
        return self.__failCssFile

    def fullCssPath(self):
        return os.path.join(self.path, self.cssFile)

    def iconsPath(self, compiled=True):
        """
        @parem compiled: When set to True it will return path to .rcc else .qrc
        """
        if self.icons is not None:
            iconFile = self.icons
            if compiled:
                iconFile = replaceFileExt(iconFile, '.rcc')
            return os.path.join(self.path, iconFile)
        return ""

    def resultsCssPath(self):
        if self.resultsCssFile is not None:
            return os.path.join(self.path, self.resultsCssFile)
        return ""

    def failCssPath(self):
        if self.failCssFile is not None:
            return os.path.join(self.path, self.failCssFile)
        return ""


class ThemesBase:
    def __init__(self):
        self.__currentTheme = ""
        self.__currentStyle = ""
        self.__themes = []
        self.__styles = []
        self.__resultsCss = ""
        self.__failCss = ""
        self.__loadedIcons = False

    """ properties
    """
    @property
    def currentTheme(self):
        return self.__currentTheme

    @property
    def currentStyle(self):
        return self.__currentStyle

    @property
    def themes(self):
        """ The available themes specific for searx-qt
        """
        return self.__themes

    @property
    def styles(self):
        """ The available system styles
        """
        return self.__styles

    @property
    def htmlCssResults(self):
        """ The css that will be includes in the results page (html).
        """
        return self.__resultsCss

    @property
    def htmlCssFail(self):
        """ The css that will be includes in the fail result page (html).
        """
        return self.__failCss

    """ class methods
    """
    def getTheme(self, key):
        for theme in self.themes:
            if theme.key == key:
                return theme
        return None

    def setStyle(self, name):
        debug(f"setSystemStyle {name}", self)
        qApp = QApplication.instance()
        if name not in self.styles:
            return
        qApp.setStyle(name)
        self.__currentStyle = name

    def setTheme(self, key):
        debug(f"setTheme {key}", self)
        qApp = QApplication.instance()

        # Unload old theme
        if self.currentTheme:
            # unload icons
            if self.__loadedIcons:
                self.__loadedIcons = False
                iconsPath = self.getTheme(self.currentTheme).iconsPath()
                if not QResource.unregisterResource(iconsPath):
                    warning(f"Failed to unregister resource {iconsPath}",
                            self)

            # reset values
            self.__resultsCss = ""
            self.__failCss = ""
            self.__loadedIcons = False
            self.__currentTheme = ""
            qApp.setStyleSheet("")

        if key not in [theme.key for theme in self.themes]:
            warning(f"Theme with key `{key}` requested but not found!", self)
            return

        if not key:
            # Do not set new theme.
            self.__currentTheme = ""
            return

        newTheme = self.getTheme(key)

        # Load icons
        if newTheme.icons:
            iconsPath = newTheme.iconsPath()
            if QResource.registerResource(iconsPath):
                self.__loadedIcons = True
            else:
                warning(f"Failed to register resource {iconsPath}", self)

        # Results CSS
        if newTheme.resultsCssFile:
            self.__resultsCss = ThemesBase.readCss(newTheme.resultsCssPath())

        # Fail CSS
        if newTheme.failCssFile:
            self.__failCss = ThemesBase.readCss(newTheme.failCssPath())

        # Load and apply the stylesheet
        cssFilePath = newTheme.fullCssPath()
        qApp.setStyleSheet(ThemesBase.readCss(cssFilePath))

        self.__currentTheme = key

    def serialize(self):
        return {
            "theme": self.currentTheme,
            "style": self.currentStyle
        }

    def deserialize(self, data):
        theme = data.get("theme", "")
        style = data.get("style", "")
        repolishNeeded = False

        if self.currentStyle != style and style in self.styles:
            self.setStyle(style)
            repolishNeeded = True

        if self.currentTheme != theme:
            self.setTheme(theme)
            repolishNeeded = True

        if repolishNeeded:
            ThemesBase.repolishAllWidgets()

    def populate(self):
        self.__themes = ThemesBase.getThemes()
        self.__styles = ThemesBase.getStyles()

    """ staticmethods
    """
    @staticmethod
    def readCss(path):
        css = ""
        try:
            cssFile = open(path, 'r')
        except OSError as err:
            warning(f"Failed to read file {path} error: {err}", ThemesBase)
        else:
            css = cssFile.read()
            cssFile.close()
        return css

    @staticmethod
    def getStyles():
        """ Returns a list with available system styles.
        """
        return QStyleFactory.keys()

    @staticmethod
    def getThemes():
        """ Will look for themes in the user's data location and system data
        location.

         Examples:
           user: ~/.local/searx-qt/themes
            sys: /usr/share/searx-qt/themes

        https://doc.qt.io/qt-5/qstandardpaths.html
        """
        return (
            ThemesBase.findThemes(os.getcwd() + "/themes/") +
            ThemesBase.findThemes(USER_THEMES_PATH) +
            ThemesBase.findThemes(SYS_THEMES_PATH)
        )

    @staticmethod
    def getCurrentStyle():
        return QApplication.instance().style().objectName()

    @staticmethod
    def findThemes(path, lookForCompiledResource=True):
        """
        @param path: Full path to the themes directory.
        @type pathL str

        @param lookForCompiledResource:
            When set to True it will exclude themes that defined a .qrc file
            and there is no .rcc file found.

            When set to False it will exclude themes that defined a .qrc file
            but the .qrc file is not found.
        @type lookForCompiledResource: bool
        """
        themes = []

        if not os.path.exists(path):
            debug(f"Themes path {path} not found.", ThemesBase)
            return themes
        elif not os.path.isdir(path):
            debug(f"Themes path {path} is not a directory.", ThemesBase)
            return themes

        for themeDir in os.listdir(path):
            fullThemePath = os.path.join(path, themeDir)

            # Get theme manifest data
            manifestFilePath = os.path.join(fullThemePath, 'manifest.json')
            if not os.path.isfile(manifestFilePath):
                error(f"{manifestFilePath} not found.", ThemesBase)
                continue

            name = ""
            appCssFile = ""
            resultsCssFile = None
            failCssFile = None
            icons = None

            try:
                manifestFile = open(manifestFilePath, 'r')
            except OSError as err:
                error(f"Could not open manifest file {manifestFilePath} " \
                      f"error: {err}", ThemesBase)
                continue
            else:
                try:
                    manifestJson = json.load(manifestFile)
                except json.JSONDecodeError as err:
                    error(f"Malformed manifest {manifestFilePath} {err}",
                          ThemesBase)
                    manifestFile.close()
                    continue
                else:
                    manifestFile.close()
                    del manifestFile

                    # manifest.json key: name (str)
                    #  - *name is a required key.
                    name = manifestJson.get('name', None)
                    if type(name) is not str:
                        error(f"Malformed manifest {manifestFilePath} name " \
                              "is not set or is not a string.", ThemesBase)
                        continue

                    styles = manifestJson.get('styles', None)
                    if type(styles) is not dict:
                        warning(
                            "manifest.json key 'style' is not a dict",
                            ThemesBase
                        )
                        continue

                    # manifest.json key: stylesheet (str)
                    #  - *stylesheet is a required key.
                    #  - It should have the .css filename that should be
                    #    present in the root of the theme directory.
                    appCssPath = styles.get('app', None)
                    if type(appCssPath) is not str:
                        error(
                            "Please set a valid value for ['styles']['app']",
                            ThemesBase
                        )
                        continue
                    appCssFile = appCssPath
                    appCssFilePath = os.path.join(fullThemePath, appCssFile)
                    if not os.path.isfile(appCssFilePath):
                        error(f"{appCssFilePath} not found.", ThemesBase)
                        continue

                    # manifest.json key: html_results (str)
                    # manifest.json key: html_fail (str)
                    resultsCssFile = styles.get('html_results', None)
                    failCssFile = styles.get('html_fail', None)

                    # manifest.json key: icons (str)
                    #  - icons is a optional key.
                    #  - Example: icons.qrc
                    #  - When set the icons.qrc file should exist in the root
                    #    of the themes directory.
                    iconsFilePath = manifestJson.get('icons', None)
                    if type(iconsFilePath) is str:
                        fullIconsFilePath = os.path.join(
                            fullThemePath,
                            iconsFilePath
                        )

                        if lookForCompiledResource:
                            # Look for .rcc instead of .qrc
                            fullIconsFilePath = replaceFileExt(
                                fullIconsFilePath,
                                '.rcc'
                            )

                        if not os.path.isfile(fullIconsFilePath):
                            warning("The theme defined a qrc/rcc resource " \
                                    "file but it could not be located! " \
                                    f"{fullIconsFilePath}", ThemesBase)
                            continue

                        icons = iconsFilePath

            del manifestFilePath

            theme = UserTheme(
                themeDir,
                name,
                appCssFile,
                fullThemePath,
                icons=icons,
                resultsCssFile=resultsCssFile,
                failCssFile=failCssFile
            )
            themes.append(theme)
        return themes

    @staticmethod
    def repolishAllWidgets():
        """ Call this after another style or theme is set to update the view.
        """
        qApp = QApplication.instance()
        for widget in qApp.allWidgets():
            widget.style().unpolish(widget)
            widget.style().polish(widget)
            widget.update()


if __name__ == '__main__':
    pass
else:
    Themes = ThemesBase()
